package physics;

import java.awt.geom.Point2D;


/**
 * This class represents a force applied in a specific direction and magnitude.
 *
 * @author Robert c. Duvall
 */
public class Force {
    /**
     * Epsilon value to add precision tolerance.
     */
    private static final double EPSILON = 0.000001;
    private static final double FORCE_DIRECTION_OFFSET = 0.001;
    private static final int DEGREES_IN_CIRCLE = 360;

    // angle in degrees
    private double myAngle;
    // "speed" in pixels per second
    private double myMagnitude;

    /**
     * Create a powerless force, i.e., with no magnitude.
     */
    public Force () {
        this(0, 0);
    }

    /**
     * Create a force in the given direction with the given magnitude.
     *
     * @param angle The angle of the force in degrees
     * @param magnitude The magnitude of the force
     */
    public Force (final double angle, final double magnitude) {
        setDirection(angle);
        setMagnitude(magnitude);
    }

    /**
     * Create a force whose direction and magnitude are determined by the
     * direction and distance between the two given points.
     *
     * @param source The start point for the force
     * @param target The end point for the force
     */
    public Force (final Point2D source, final Point2D target) {
        this(angleBetween(target, source), distanceBetween(target, source));
    }

    /**
     * Create a force that is identical to the given other force.
     *
     * @param other The force being copied from
     */
    public Force (final Force other) {
        this(other.getDirection(), other.getMagnitude());
    }

    /**
     * Reset this force to zero.
     */
    public final void reset () {
        setDirection(0);
        setMagnitude(0);
    }

    /**
     * Returns this force's magnitude (in pixels).
     *
     * @return The magnitude of the force
     */
    public final double getMagnitude () {
        return myMagnitude;
    }

    /**
     * Returns this force's magnitude relative to the given other force. More
     * formally, returns the magnitude of this force projected onto the given
     * other force.
     *
     * @param other Force to be compared to
     * @return Relative magnitude compared to other force
     */
    public final double getRelativeMagnitude (final Force other) {
        return -getMagnitude()
                * Math.cos(Math.toRadians(getAngleBetween(other)));
    }

    /**
     * Scales this force's magnitude by the given change value.
     * <UL>
     * <LI>A value of 1 leaves the magnitude unchanged
     * <LI>Values less than 1 reduce the magnitude
     * <LI>Values greater than 1 increase the magnitude
     * </UL>
     *
     * @param change The scalar number to scale the force's components by
     */
    public final void scale (final double change) {
        setMagnitude(getMagnitude() * change);
    }

    /**
     * Sets this force's magnitude to the given value.
     *
     * @param value New value for magnitude
     */
    protected final void setMagnitude (final double value) {
        myMagnitude = value;
    }

    /**
     * Returns this force's direction (in degrees).
     *
     * @return Direction of the force
     */
    public final double getDirection () {
        // standardize between -360 and +360 (keep 360, -360, and 0 as distinct
        // values)
        double sign;
        if (myAngle < 0) {
            sign = 1;
        }
        else {
            sign = -1;
        }
        return ((myAngle + sign * FORCE_DIRECTION_OFFSET) % DEGREES_IN_CIRCLE)
                - sign * FORCE_DIRECTION_OFFSET;
    }

    /**
     * Returns the angle between this force and the given other force.
     *
     * @param other Force to find the angle between
     * @return Angle between this force and the other force in degrees
     */
    public final double getAngleBetween (final Force other) {
        return getDirection() - other.getDirection();
    }

    /**
     * Adjusts this force's direction by the given change value.
     *
     * @param change The change in angle for the force
     */
    public final void turn (final double change) {
        setDirection(getDirection() + change);
    }

    /**
     * Sets this force's direction to the given value.
     */
    protected final void setDirection (final double value) {
        myAngle = value;
    }

    /**
     * Returns the change in the X direction represented by this force.
     *
     * @return The x component of this force
     */
    public final double getXChange () {
        return getMagnitude() * Math.cos(Math.toRadians(getDirection()));
    }

    /**
     * Returns the change in the Y direction represented by this force.
     *
     * @return The y component of this force
     */
    public double getYChange () {
        return getMagnitude() * Math.sin(Math.toRadians(getDirection()));
    }

    /**
     * Returns a force that is the sum of this force and the given other force.
     *
     * @param other Force to add to this one
     */
    public final void sum (final Force other) {
        // more readable, although slightly slower
        double dx = getXChange() + other.getXChange();
        double dy = getYChange() + other.getYChange();
        setDirection(angleBetween(dx, dy));
        setMagnitude(distanceBetween(dx, dy));
    }

    /**
     * Returns a force that is the difference between this force and the given
     * other force.
     *
     * @param other Force to subtract from this force
     */
    public final void difference (final Force other) {
        other.negate();
        sum(other);
    }

    /**
     * Returns a force of the same magnitude, but in the opposite direction as
     * this force.
     */
    public final void negate () {
        turn(DEGREES_IN_CIRCLE / 2);
    }

    /**
     * Returns the average of this force with the given other force.
     *
     * @param other Force to average this force with
     * @return Average magnitude and direction of this force and the other force
     */
    public final Force average (final Force other) {
        return new Force((getDirection() + other.getDirection()) / 2.0,
                (getMagnitude() + other.getMagnitude()) / 2.0);
    }

    /**
     * Return true if this force has the same magnitude and direction as the
     * given other force.
     *
     * @param force Force to check if equal to
     * @return true if forces are equal; false if not equal
     */
    @Override
    public boolean equals (final Object force) {
        try {
            Force other = (Force) force;
            return Math.abs(getMagnitude() - other.getMagnitude()) < EPSILON
                    && Math.abs(
                            getDirection() - other.getDirection()) < EPSILON;
        }
        catch (ClassCastException e) {
            return false;
        }
    }

    @Override
    public int hashCode() {
        return super.hashCode();
    }

    @Override
    public String toString () {
        return String.format("(%1.2f, %1.2f)", getDirection(), getMagnitude());
    }

    /**
     * Returns the distance between given two points.
     *
     * @param p1 One Point to find distance between
     * @param p2 Other Point to find distance between
     * @return Distance between two points
     */
    public static double distanceBetween (final Point2D p1, final Point2D p2) {
        return distanceBetween(p1.getX() - p2.getX(), p1.getY() - p2.getY());
    }

    /**
     * Returns the distance represented by the given dx and dy.
     *
     * @param dx change in x-component
     * @param dy change in x-component
     * @return Distance given by components from change in position
     */
    public static double distanceBetween (final double dx, final double dy) {
        return Math.sqrt(dx * dx + dy * dy);
    }

    /**
     * Returns the angle between the given two points.
     *
     * @param p1 One Point to find angle between
     * @param p2 Other Point to find angle between
     * @return Angle between two points in degrees
     */
    public static double angleBetween (final Point2D p1, final Point2D p2) {
        return angleBetween(p1.getX() - p2.getX(), p1.getY() - p2.getY());
    }

    /**
     * Returns the angle represented by the given dx and dy.
     *
     * @param dx x-component for vector
     * @param dy y-component for vector
     * @return Angle given by components from change in position
     */
    public static double angleBetween (final double dx, final double dy) {
        return Math.toDegrees(Math.atan2(dy, dx));
    }
}
